﻿//_______________________________________________________________________________________________________________________
// Copy Right (c) 2010, 2011 SapiensIT Consulting GmbH, all rights reserved
//_______________________________________________________________________________________________________________________
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT
// NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
//_______________________________________________________________________________________________________________________
// PROJECT:     sapiens.at.SharePoint (SharePoint 2010/V4)
//_______________________________________________________________________________________________________________________
// VERSION:     2.0
//_______________________________________________________________________________________________________________________
// DESCRIPTION: 
//_______________________________________________________________________________________________________________________
// LICENSE:     Different licens Types apply to different file types:
//               - Source code files are governed by the MICROSOFT PUBLIC LICENSE (Ms-PL)
//               - Binary files are governed by MSDN CODE GALLERY BINARY LICENSE
//               - Documentation files are governed by CREATIVE COMMONS ATTRIBUTION 3.0 LICENSE
//
//               The licenses can be found at the projects source location.
//               If you do not agree, don't use it!
// 
//_______________________________________________________________________________________________________________________
// Start SharePoint Branding, visit http://www.cliggin.com! Visit us on http://www.sapiens.at! Tell your friends! 
//_______________________________________________________________________________________________________________________
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
using System.Collections;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using Sapiens.at.SharePoint.Extensions;
using Sapiens.at.SharePoint.Entities;
using Sapiens.at.SharePoint.Security;
using Sapiens.at.SharePoint.Utils;

namespace Sapiens.at.SharePoint.Receiver
{
    /// <summary>
    /// Defines callback methods that get called from the event receiver.
    /// </summary>
    /// <param name="args">Informations about the event</param>
    public delegate void ItemEventDelegate(ItemEventArgs args);

    /// <summary>
    /// Base class for event receiver that handle events on listitems.
    /// </summary>
    public abstract class BaseItemReceiver : SPItemEventReceiver
    {
        #region Properties
        /// <summary>
        /// Stores all registered events grouped by the SPEventReceiverType
        /// </summary>
        protected Dictionary<SPEventReceiverType, List<ItemEventConfig>> ItemEvents { get; set; }
        #endregion

        #region Constructors

        /// <summary>
        /// Gets called from SharePoint before the events occur.
        /// </summary>
        public BaseItemReceiver()
        {
            ItemEvents = new Dictionary<SPEventReceiverType, List<ItemEventConfig>>();
        }

        #endregion

        #region Methods

        /// <summary>
        /// Registers a new item event with the provided configuration.
        /// </summary>
        /// <param name="itemEventConfig"></param>
        protected void AddItemEventConfig(ItemEventConfig itemEventConfig)
        {
            foreach (SPEventReceiverType eventType in itemEventConfig.EventTypes)
            {
                List<ItemEventConfig> itemEvent;
                if (ItemEvents.ContainsKey(eventType))
                {
                    itemEvent = ItemEvents[eventType];
                }
                else
                {
                    itemEvent = new List<ItemEventConfig>();
                    ItemEvents.Add(eventType, itemEvent);
                }
                // insert the event config according to the priority
                int i;
                for (i = 0; i < itemEvent.Count; i++)
                {
                    if (itemEventConfig.Priority < itemEvent[i].Priority) { break; }
                }
                itemEvent.Insert(i, itemEventConfig);
            }
        }

        /// <summary>
        /// Finds and invokes the registered event handler. Stores the changed values in the synchronous event and fetches them in the asynchronous event.
        /// </summary>
        /// <param name="properties"></param>
        protected virtual void ProcessEvent(SPItemEventProperties properties)
        {
            using (SPWeb web = properties.OpenWeb())
            {
                //check if contenttype is different to the contenttypeid
                SPContentTypeId contentTypeId = SPContentTypeId.Empty;
                if (!string.IsNullOrEmpty(Convert.ToString(properties.AfterProperties["ContentTypeId"])))
                {
                    contentTypeId = new SPContentTypeId(Convert.ToString(properties.AfterProperties["ContentTypeId"]));
                }
                else if (properties.ListItem != null)
                {
                    contentTypeId = properties.ListItem.ContentTypeId;
                }

                var cTypeName = Convert.ToString(properties.AfterProperties["ContentType"]);
                if (!string.IsNullOrEmpty(cTypeName))
                {
                    var contenttype = web.Lists[properties.ListId].ContentTypes.GetContentType(contentTypeId);
                    if (contenttype == null || (cTypeName != contenttype.Name && web.Lists[properties.ListId].ContentTypes[cTypeName] != null))
                    {
                        contenttype = !string.IsNullOrEmpty(cTypeName) ? web.Lists[properties.ListId].ContentTypes[cTypeName] : web.Lists[properties.ListId].ContentTypes[0];
                        if (contenttype != null) properties.AfterProperties["ContentTypeId"] = contenttype.Id.ToString();
                    }
                }

                bool doUpdate = false;
                bool doPrivilegedUpdate = false;
                bool enableEvents = false;
                var entityAssembly = Assembly.GetAssembly(GetType());
                var entity = ListItemMapper.GetEntity(properties, web, entityAssembly);

                // check for content type limitations
                var contentTypeLimit = GetType().GetCustomAttributes(typeof(LimitToContentTypeAttribute), true).Cast<LimitToContentTypeAttribute>();
                if (contentTypeLimit.Count() > 0 && !contentTypeLimit.Any(l => l.ContentTypeIds.Any(cid => cid.IsParentOf(entity.ContentTypeId))))
                { // Receiver class is limited to a different contenttype -> exit
                    return;
                }

                Dictionary<string, object> changedFields = null;
                // order the after-properties to get the correct hash value
                var afterProperties = properties.AfterProperties.Cast<DictionaryEntry>().OrderBy(ap => ap.Key.ToString()).ToDictionary(ap => ap.Key.ToString(), ap => ap.Value);
                switch(properties.EventType)
                {
                    case SPEventReceiverType.ItemUpdating:
                        changedFields = afterProperties
                         .Where(ap => IsChanged(ap.Key, ap.Value, properties.ListItem))
                         .ToDictionary(ap => ap.Key, ap => properties.ListItem != null && properties.ListItem.Fields.Cast<SPField>().Any(f => f.InternalName == ap.Key) ? properties.ListItem[ap.Key] : null);
                        break;
                    case SPEventReceiverType.ItemAdding:
                    case SPEventReceiverType.ItemAdded:
                        changedFields = afterProperties.ToDictionary(ap => ap.Key, ap => default(object));
                        break;
                    case SPEventReceiverType.ItemDeleting:
                    case SPEventReceiverType.ItemDeleted:
                        changedFields = new Dictionary<string, object>();
                        break;
                    default:
                        changedFields = afterProperties.ToDictionary(ap => ap.Key, ap => properties.ListItem != null && properties.ListItem.Fields.Cast<SPField>().Any(f=>f.InternalName == ap.Key) ? properties.ListItem[ap.Key] : null);
                        break;
                }
                System.Threading.Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(web.CurrencyLocaleID);

                Permissions.RunWithElevatedPrivileges(properties.SiteId, web.ID, properties.ListItem, delegate(SPWeb privilegedWeb, SPListItem privilegedListItem)
                {
                    // instantiate a privileged listitem
                    var privilegedEntity = properties.ListItem.IsCheckedOut() ? null : ListItemMapper.GetEntity(properties, privilegedListItem, privilegedWeb, entityAssembly);
                    if (ItemEvents.ContainsKey(properties.EventType))
                    {
                        foreach (var itemEvent in ItemEvents[properties.EventType])
                        {
                            if (IsMatchingItemEvent(itemEvent, changedFields.Keys, contentTypeId))
                            { // a registered event handler has been found which meets the current event properties
                                // set up the event argument
                                var args = new ItemEventArgs(itemEvent.Privileged ? privilegedEntity : entity, changedFields, itemEvent.Privileged ? privilegedWeb : web, properties);
                                // call the event handler callback
                                try
                                {
                                    itemEvent.Callback(args);
                                }
                                catch (Exception ex)
                                {
                                    properties.Cancel = true;
                                    properties.ErrorMessage = ex.Message;
                                    throw ex;
                                }
                                if (args.DoUpdate)
                                {
                                    // store, whether this listitem needs an update
                                    if (args.EnableEvents) { enableEvents = true; }
                                    if (itemEvent.Privileged) { doPrivilegedUpdate = true; } else { doUpdate = true; }
                                }
                                // if set, cancel the processing and prevent from updating
                                if (args.Cancel)
                                {
                                    properties.Cancel = true;
                                    properties.ErrorMessage = args.CancelErrorMessage;
                                    properties.Status = string.IsNullOrEmpty(args.CancelErrorMessage) ? SPEventReceiverStatus.CancelNoError : SPEventReceiverStatus.CancelWithError;
                                    doUpdate = doPrivilegedUpdate = false;
                                    break;
                                }
                            }
                        }
                        // update if necessary
                        if (doUpdate) { entity.SystemUpdate(enableEvents); }
                        if (doPrivilegedUpdate && privilegedEntity != null) { privilegedEntity.SystemUpdate(enableEvents); }
                    }
                });
            }
        }

        /// <summary>
        /// Helper method to determine if a field has changed.
        /// </summary>
        /// <param name="fieldName"></param>
        /// <param name="value"></param>
        /// <param name="listItem"></param>
        /// <returns></returns>
        private bool IsChanged(string fieldName, object value, SPListItem listItem)
        {
            if (listItem == null) { return !string.IsNullOrEmpty(value.ToString()); }
            SPField Field;
            Type fieldType;
            try
            {
                Field = listItem.Fields.GetFieldByInternalName(fieldName);
                fieldType = Field.FieldValueType;
            }
            catch (ArgumentException) { return false; }
            try
            {
                if (fieldType == typeof(DateTime))
                {
                    if (String.IsNullOrEmpty(Convert.ToString(value)))
                    {
                        return Conversion.SafeCast<DateTime>(listItem[fieldName]) != DateTime.MinValue;
                    }
                    return !Conversion.SafeCast<DateTime>(listItem[fieldName]).Equals(Conversion.SafeCast<DateTime>(Convert.ToString(value)));
                }
                else if (fieldType == typeof(bool))
                {
                    bool bValue = false;
                    if (!bool.TryParse(Convert.ToString(value), out bValue))
                    {
                        int iValue = 0;
                        if (int.TryParse(Convert.ToString(value), out iValue))
                            bValue = (iValue == 1) ? true : false;
                    }
                    return bValue != Convert.ToBoolean(listItem[fieldName]);
                }
                else if (fieldType == typeof(SPFieldLookupValue) || fieldType == typeof(SPFieldUserValue))
                {
                    //check if the FieldValue is a valid lookupvalue, if not return false;
                    if (!IsValidLookupValue(Convert.ToString(listItem[fieldName])) || !IsValidLookupValue(Convert.ToString(value))) return false;
                    return new SPFieldLookupValue(Convert.ToString(value)).LookupId != new SPFieldLookupValue(Convert.ToString(listItem[fieldName])).LookupId;
                }
                else if (fieldType == typeof(SPFieldLookupValueCollection) || fieldType == typeof(SPFieldUserValueCollection))
                {
                    return !new SPFieldLookupValueCollection(Convert.ToString(value)).SequenceEqual(new SPFieldLookupValueCollection(Convert.ToString(listItem[fieldName])), new LookupComparer());
                }
                else if ((Field.Type == SPFieldType.Note) && (Field as SPFieldMultiLineText).RichText)
                {
                    //ignore html format
                    //compare plain text
                    var propertyValue = Conversion.SafeCast<string>(listItem[fieldName]);
                    if (String.IsNullOrEmpty(propertyValue) && String.IsNullOrEmpty(Convert.ToString(value))) { return false; }
                    propertyValue = Microsoft.SharePoint.Utilities.SPHttpUtility.ConvertSimpleHtmlToText(propertyValue, propertyValue.Length);
                    var ItemValue = Convert.ToString(value);
                    ItemValue = Microsoft.SharePoint.Utilities.SPHttpUtility.ConvertSimpleHtmlToText(ItemValue, ItemValue.Length);
                    return propertyValue != ItemValue;
                }
                else
                {
                    var propertyValue = Conversion.SafeCast<string>(listItem[fieldName]);
                    if (String.IsNullOrEmpty(propertyValue) && String.IsNullOrEmpty(Convert.ToString(value))) { return false; }
                    return propertyValue != Convert.ToString(value);
                }
            }
            catch (InvalidCastException)
            { // something went wrong. Return true, so that an eventual event handler gets rather fired then not.
                return true;
            }
        }
        /// <summary>
        /// checks if the given value is a valid SPFieldLookupValue
        /// </summary>
        /// <param name="Value">LookupValue</param>
        /// <returns>true if the value can be parsed to SPFieldLookupValueCollection, otherwise false</returns>
        private bool IsValidLookupValue(string Value)
        {
            try
            {
                SPFieldLookupValue lv = new SPFieldLookupValue(Value);
                return true;
            }
            catch { return false; }
        }

        /// <summary>
        /// Helper method to determine if the event config meets the current event properties
        /// </summary>
        /// <param name="itemEvent"></param>
        /// <param name="changedFields"></param>
        /// <param name="contentTypeId"></param>
        /// <returns></returns>
        private bool IsMatchingItemEvent(ItemEventConfig itemEvent, IEnumerable<string> changedFields, SPContentTypeId contentTypeId)
        {
            return (itemEvent.ContentTypeIDs == null || itemEvent.ContentTypeIDs.Count() == 0 || itemEvent.ContentTypeIDs.Any(c => c.IsParentOf(contentTypeId))) &&
                            (itemEvent.OnChangedFields == null || itemEvent.OnChangedFields.Count() == 0 ||
                                (itemEvent.AllChangedFieldsMustMatch && itemEvent.OnChangedFields.All(cf => changedFields.Contains(cf))) ||
                                (!itemEvent.AllChangedFieldsMustMatch && itemEvent.OnChangedFields.Any(cf => changedFields.Contains(cf))));
        }

        #endregion

        #region Receiver Events

        /// <summary>
        /// Standard Sharepoint event method. Starts the internal processing.
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAdded(SPItemEventProperties properties)
        {
            base.ItemAdded(properties);
            if (properties.ListItem != null)
            {
                ProcessEvent(properties);
            }
        }

        /// <summary>
        /// Standard Sharepoint event method. Starts the internal processing.
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemAdding(SPItemEventProperties properties)
        {
            base.ItemAdding(properties);
            ProcessEvent(properties);
        }

        /// <summary>
        /// Standard Sharepoint event method. Starts the internal processing.
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemDeleting(SPItemEventProperties properties)
        {
            base.ItemDeleting(properties);
            if (properties.ListItem != null)
            {
                ProcessEvent(properties);
            }
        }

        /// <summary>
        /// Standard Sharepoint event method. Starts the internal processing.
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemUpdated(SPItemEventProperties properties)
        {
            base.ItemUpdated(properties);
            if (properties.ListItem != null)
            {
                ProcessEvent(properties);
            }
        }

        /// <summary>
        /// Standard Sharepoint event method. Starts the internal processing.
        /// </summary>
        /// <param name="properties"></param>
        public override void ItemUpdating(SPItemEventProperties properties)
        {
            base.ItemUpdating(properties);
            if (properties.ListItem != null)
            {
                ProcessEvent(properties);
            }
        }

        #endregion
    }
}
